2013年12月06日
_ Ruby でバイナリデータを扱う方法
この記事は KMC Advent Calendar 2013 の 6 日目の記事です。 昨日は tsutcho 君の CUDAのメモリの種類が多くて面倒だという話 でした。
この記事ではRuby でバイナリデータを扱う方法について解説します.Gitlab→PT2 on Linux→LINQ→スペインでお買い物→CUDA と来た次です. String#unpackとか結構知られていないようなので,そのあたりについて解説します. あまり長々と解説しても記事を読むのが面倒になるだけなので, ここでは読み込みだけを扱います.
普段Rubyでバイナリデータを扱うことはあまりないでしょう. しかし,
- ちょっとした画像データの処理をしたいけど Image magick をインストールする ほどではない
- 入出力にバイナリデータしか受け付けないソフトウェア用のツールの作成
- バイナリデータを入出力するソフトのテストやデバッグ
といった場合にはバイナリデータをRubyで扱えると便利です.
encoding: ASCII-8BIT
Ruby ではバイナリデータを文字列として扱います.
このとき,文字列のエンコーディングに ASCII-8BIT を指定します. これによってその文字列がバイナリであることをマークします. こうすることによって String#[] が n 番目の文字を返すのではなく n 番目のバイト(を表わすStringオブジェクト)を返すようになります. またバイナリデータを等号で比べるときには,両方の文字列の エンコーディングを ASCII-8BIT で揃えておく必要があります. エンコーディングの指定方法自体は UTF-8 や cp932 などと同じで, IO.open の引数で指定したりします.
Ruby 2.0 以降には String#b というメソッドがあり,これは 文字列の中身(バイト列)は同じでエンコーディングが ASCII-8BIT である データを返します.特にリテラル文字列でバイト列を指定するときに 便利です.例えば
png_magic_8bytes = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a".b
などとします(このバイト列は PNG フォーマットの最初8バイトのマジックナンバです).
String#unpack
バイナリデータフォーマットでは,往々にして「ここから4バイトは,big endian の 符号なし整数で x のサイズを表し,次の4バイトは同じbig endian の 符号なし整数で y のサイズを表す.次の1バイトは0か1か2のいずれかで データの種類を表す…」という構造をしています.ASCII-8BITな文字列を このように解釈するのが String#unpack というメソッドです.
このメソッドはテンプレート文字列によってバイト列のフォーマットを指定し, 文字列をそのフォーマットに従って解釈して変換します. 例えば上の例では "NNC" と指定します.
詳しくはリファレンスのString#unpackの項 を参照してください.
例
ここでは,PNGフォーマットの画像ファイルのサイズ(幅×高さ)と1画素あたりのビット数を 表示するプログラムを作りましょう.
PNGの仕様は RFC2083 で定義されています. 仕様の必要な部分を解説すると,
- データは最初8バイトにPNGを表すマジックナンバがあり, そこからチャンクと呼ばれるデータの塊が複数続く
- 各チャンクの最初4バイトはチャンクのサイズ(バイト数),次の4バイトは チャンクの種類で,そこから先がチャンクのデータ
- 最初のチャンクは "IHDR" チャンクで,13バイトのデータであり,以下の
内容がこの順で収められている
- 画像幅(4バイト)
- 画像高さ(4バイト)
- 色深度(1バイト)
- カラータイプ(1バイト)
- 圧縮形式(1バイト)
- フィルタの種類(1バイト)
- インターレース(1バイト)
- すべての整数データはビッグエンディアン
読み出したいのは画像の幅,高さ,色深度なので,IHDRチャンクのその部分を読み込めばよい わけです.
プログラムは以下の通りです.
exit if ARGV.empty? # ファイルのエンコーディングに ASCII-8BIT を指定する file = File.open(ARGV[0], "r:ASCII-8BIT") # PNGの最初の8ビットのマジックナンバ png_magic_8bytes = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a".b # 最初8バイトを File#read で読み込んで png_magic_8bytes と一致しているかを調べる raise "This file is not a PNG file." if file.read(8) != png_magic_8bytes # 最初のチャンクの8バイトのヘッダ部を読み込んで first_chunk_size, first_chunk_type = file.read(8).unpack("Na4") # 最初のチャンクは"IHDR"で13バイトと決まっているのでそれを確認する raise "First chunk should be IHDR" if first_chunk_type != "IHDR".b || first_chunk_size != 13 # 続く13バイトを読み込む chunk = file.read(first_chunk_size) # 13バイトのバイト列を整数に変換する width, height, bit_depth, color_type, compression_method, filter_method, interlace_method = chunk.unpack("NNCCCCC") # 各情報を表示 puts "image size: #{width}x#{height} pixels" puts "depth: #{bit_depth} bits" file.close
さらなる勉強のために
整数データをバイナリ文字列に変換するためには Array#pack を使います. これは String#unpack と逆向きの変換を行います. これによってバイナリデータの書き込みができます.
明日は dis 君の @生活の知恵@ です。