SSブログ

rubyでバイナリファイルを読んでみる [プログラミング]

■はじめに

何か新しく言語を勉強したいと思い、オブジェクト指向もいっしょに勉強できそうな ruby を選んでみました。

さっと入門サイトで勉強してみたのですが、文法をいくら教えてもらったところで実践しないと身につかないものです。

題材は何がいいか悩んだのですが、仕事でも関係しているGDSIIフォーマットというバイナリファイルのパーサを作ってみることにしました。

明らかにスクリプト言語には不向きと思われるのですが、ざっと調べるとバイナリファイルの取扱もできそうですし、GDSIIは十数種の可変長レコードの塊で、各レコードが階層構造を構成したりしているので、それらをオブジェクトとして扱えたりするのでは、という期待もありました。


■GDSIIフォーマット

本当にrubyでGDSIIを扱いたいのなら、すでにそういうライブラリが存在するようです。

今回は勉強が目的なので、スクラッチで組み上げます。

GDSIIフォーマットについては、こちらのサイトがまとまってて助かりました。


■パーサ

いちおうGDSIIファイルを読んで、各レコードの情報を取り出せるようになりました。パースするところまでで、その先の各レコードの階層構造を構築するところはできていません。

なにしろruby初心者なので、「rubyらしく」実装できていないと思います。各レコードをクラスにしてはみましたが、オブジェクト指向をほとんど理解していないので、どこまで有効な設計になっているか、はなはだ疑問です。このあたりは一人でやっていても駄目ですね。


#!/usr/bin/env ruby
#
# stream.rb -- GDSII Data Parser
#  "Stream Format" is output format of GDSII data.
#
#  Todo:
#   - do like ruby.
#   - probably bug about upper/lower nibble
#
#  Reference:
#   GDSII Format: http://www7b.biglobe.ne.jp/~garaku/HSGDS/GDSII_p01.html
#
#  History:
#   0.2        change method to class of each record, add Colrow
#   0.1        implement some records will be used usually
#
#  Author:
#   M.Hori   

####################
# Constant
####################

# Variables
MY_VERSION = '0.2'
USAGE_MSG = "Usage: " << $PROGRAM_NAME << " [options] GDSII_File"

####################
# Module
####################

module Util
    # Data Converter
    def int2(input)
        input.unpack("n")[0]
    end

    def int4(input)
        input.reverse.unpack("l*")[0]
    end

    def real4(input)
    end

    def real8(input)
        # S=sign, E=index, M=mantissa
        #  SEEEEEEE MMMMMMMM MMMMMMMM ...(total 7 byte)... MMMMMMMM
        # real8 = (-1)^sign * (0.mantissa)_2 * 16^(index-64)
        sign = (input & 0x8000000000000000)>>63
        index = (input & 0x7F00000000000000)>>56
        mantissa = (input & 0x00FFFFFFFFFFFFFF)/(2.0**56)
        ((-1)**sign)*(mantissa*16**(index-64))
    end

    def ascii(input)
        input.unpack("A*")
    end

    # long print
    def lpr(*var)
        printf(*var) if $long
    end

    module_function :int2, :int4, :real4, :real8, :ascii, :lpr
end

####################
# クラス
####################

class Record
    # 2byte ==> unpack("n")    # convert to big endian (byte order)
    # 1byte ==> unpack("c")
    # +----------------------+
    # |         size         | 2 byte
    # +-----------+----------+               --+
    # |record_type|_data_type| 1 byte + 1 byte |
    # +-----------+----------+                 +-- others
    # |         data         | (size-4) byte   |
    # |           :          |                 |
    # +----------------------+               --+

    # readable instance variables
    attr_reader :record_type, :_data_type, :data

    # Array
    @@record_type_array = [
        "HEADER",   "BGNLIB",   "LIBNAME",   "UNITS",       "ENDLIB",
        "BGNSTR",   "STRNAME",  "ENDSTR",    "BOUNDARY",    "PATH",
        "SREF",     "AREF",     "TEXT",      "LAYER",       "DATATYPE",
        "WIDTH",    "XY",       "ENDEL",     "SNAME",       "COLROW",
        "TEXTNODE", "NODE",     "TEXTTYPE",  "PRESENTATION","SPACING",
        "STRING",   "STRANS",   "MAG",       "ANGLE",       "UNITEGER",
        "USTRING",  "REFLIBS",  "FONTS",     "PATHTYPE",    "GENERATIONS",
        "ATTRTABLE","STYPTABLE","STRTYPE",   "ELFLAGS",     "ELKEY",
        "LINKTYPE", "LINKKEYS", "NODETYPE",  "PROPATTR",    "PROPVALUE",
        "BOX",      "BOXTYPE",  "PLEX",      "BGNEXTN",     "ENDEXTN",
        "TAPENUM",  "TAPECODE", "STRCLASS",  "RESERVED",    "FORMAT",
        "MASK",     "ENDMASKS", "LIBDIRSIZE","SRFNAME",     "LIBSECUR",
    ]
   
    @@data_type_array = [
        "NO_DATA", "BIT_ARRAY", "INT_2", "INT_4", "REAL_4",
        "REAL_8",  "STRING",
    ]

    # Initializer
    def initialize(size, others)
        @size = size
        @record_type, @_data_type = others[0,2].unpack("cc")
        @data = others[2,size-4]    # size=2, record=1, data=1, total=4byte
    end

    # to_s
    def to_s
        return    @@record_type_array[@record_type],
                @@data_type_array[@_data_type]
    end

    # no_operation
    def no_operation
        Util.lpr(" --------------------")
    end

    # Parser
    def parse_data
        case record_type
        when  0    # HEADER
            Util.lpr(" GDSII_Version=%s", Header.new(@data).gds_version)
        when  1    # BGNLIB
            _m, _a = Bgnlib.new(@data).date
            Util.lpr(" Mod=%s Acc=%s", _m, _a)
        when  2 # LIBNAME
            Util.lpr(" LibraryName=%s", Libname.new(@data).name)
        when  3 # UNITS
            #units()
            _uu, _um = Units.new(@data).units
            Util.lpr(" dbUnitByUserUnit=%.1e dbUnitByMeter=%.1e", _uu, _um)
        when  4 # ENDLIB
            no_operation()
        when  5 # BGNSTR
            $log["structures"] += 1
            _m, _a = Bgnstr.new(@data).date
            Util.lpr(" Mod=%s Acc=%s", _m, _a)
        when  6 # STRNAME
            Util.lpr(" StructureName=%s", Strname.new(@data).name)
        when  7 # ENDSTR
            no_operation()
        when  8 # BOUNDARY
            $log["boundaries"] += 1
            no_operation()
        when  9    # PATH
            $log["paths"] += 1
            no_operation()
        when 10    # SREF
            $log["srefs"] += 1
            no_operation()
        when 11    # AREF
            $log["arefs"] += 1
            no_operation()
        when 12    # TEXT
            $log["texts"] += 1
            no_operation()
        when 13 # LAYER
            Util.lpr(" LayerNumber=%d", Layer.new(@data).number)
        when 14 # DATATYPE
            Util.lpr(" DataType=%d", Datatype.new(@data).number)
        when 15    # WIDTH
            Util.lpr(" Width=%.1f", Width.new(@data).width)
        when 16 # XY
            Util.lpr(" %s", Xy.new(@data).xy_list)
        when 17 # ENDEL
            no_operation()
        when 18    # SNAME
            Util.lpr(" StructureName=%s", Sname.new(@data).name)
        when 19    # COLROW
            _col, _row = Colrow.new(@data).colrow
            Util.lpr(" Col=%d, Row=%d", _col, _row)
        when 22    # TEXTTYPE
            Util.lpr(" Text_Type=%d", Texttype.new(@data).number)
        when 23    # PRESENTATION
            _obj = Presentation.new(@data)
            Util.lpr(" Font#=%d Origin=%s", _obj.font, _obj.origin)
        when 25    # STRING
            Util.lpr(" String=%s", Strings.new(@data).name)
        when 26    # STRANS
            _obj = Strans.new(@data)
            Util.lpr(" Mirror=%s Mag=%d Angle=%d",
                _obj.mirror_flag, _obj.mag_flag, _obj.angle_flag)
        when 27    # MAG
            Util.lpr(" Mag=%.1e", Mag.new(@data).value)
        when 28    # ANGLE
            Util.lpr(" Angle=%.1f", Angle.new(@data).value)
        when 33    # PATHTYPE
            Util.lpr(" PathType=%d", Pathtype.new(@data).number)
        when 48    # BGNEXTN
            Util.lpr(" ProjectionSize=%.1f", Bgnextn.new(@data).width)
        when 49    # ENDEXTN
            Util.lpr(" ProjectionSize=%.1f", Endextn.new(@data).width)
        end
    end
end

# HEADER
class Header
    def initialize(data)
        @data = data
    end

    def gds_version
        case @data.unpack("n")[0]
        when 0   then @ver = "v3.0"
        when 3   then @ver = "v3.0"
        when 4   then @ver = "v4.0"
        when 5   then @ver = "v5.0"
        when 600 then @ver = "v6.0"
        else          @ver = "unknown"
        end
        @ver
    end
end

# BGNLIB
class Bgnlib
    def initialize(data)
        @data = data
    end

    def date
        my,mm,md,mh,mn,ms,ay,am,ad,ah,an,as = @data.unpack("n12")
        @last_modify =
            "%4d/%02d/%02d,%02d:%02d:%02d"%([1900+my,mm,md,mh,mn,ms])
        @last_access =
            "%4d/%02d/%02d,%02d:%02d:%02d"%([1900+ay,am,ad,ah,an,as])
        return @last_modify, @last_access
    end
end

# LIBNAME
class Libname
    def initialize(data)
        @data = data
    end

    def name
        Util.ascii(@data)
    end
end

# UNITS
class Units
    def initialize(data)
        @data = data
    end

    def units
        # unpack("H16") H: Hex(upper nibble first)
        @unit_by_uu = Util.real8(@data[0, 8].unpack("H16").join.hex)
        @unit_by_meter = Util.real8(@data[8, 8].unpack("H16").join.hex)
        return @unit_by_uu, @unit_by_meter
    end
end

# BGNSTR
class Bgnstr < Bgnlib
end

# STRNAME
class Strname < Libname
end

# LAYER
class Layer
    def initialize(data)
        @data = data
    end

    def number
        Util.int2(@data)
    end
end

# DATATYPE
class Datatype < Layer
end

# WIDTH
class Width
    def initialize(data)
        @data = data
    end

    def width
        Util.int4(@data)
    end
end

# XY
class Xy
    def initialize(data)
        @data = data
    end

    def xy_list
        # unpack("l*") : long(32bit signed int)
        # reverse : convert endian
        # map(&:to_i) : convert array to int
        xys = @data.reverse.unpack("l*").map(&:to_i)
        @xy_list = ""
        xys.each_slice(2) do |x, y|
            @xy_list <<= "(" << x.to_s << "," << y.to_s << ")"
            # '<<' beter more than '+' for cat strings
        end
        @xy_list
    end
end

# SNAME
class Sname < Libname
end

# COLROW
class Colrow
    def initialize(data)
        @data = data
    end

    def colrow
        @data.unpack("n2")
    end
end

# TEXTTYPE
class Texttype < Layer
end

# PRESENTATION
class Presentation
    def initialize(data)
        @data = data
    end

    def font
        @data.unpack("c2")[0].to_i
    end

    def origin
        text_origin_array = [
            "upperLeft",  "upperCenter",  "upperRight",  nil,
            "centerLeft", "centerCenter", "centerRight", nil,
            "lowerLeft",  "lowerCenter",  "lowerRight",  nil
        ]
        text_origin_array[@data.unpack("c2")[1].to_i]
    end
end

# STRING
class Strings < Libname
end

# STRANS
class Strans
    def initialize(data)
        @data = data
    end

    def mirror_flag
        (@data.unpack("c2")[0] & 0x80)>>7
    end

    def mag_flag
        (@data.unpack("c2")[1] & 0x04)>>2
    end

    def angle_flag
        (@data.unpack("c2")[1] & 0x02)>>1
    end
end

# MAG
class Mag
    def initialize(data)
        @data = data
    end

    def value
        Util.real8(@data[0, 8].unpack("H16").join.hex)
    end
end

# ANGLE
class Angle < Mag
end

# PATHTYPE
class Pathtype < Layer
    # 0=no projection,1=half round projection,
    # 2=half width projection, 4=width is BGNEXTN,ENDEXTN
end

# BGNEXTN
class Bgnextn < Width
end

# ENDEXTN
class Endextn < Width
end

####################
# Main
####################

# Variables
$log = {
    "records"         => 0,
    "structures"    => 0,
    "boundaries"    => 0,
    "paths"            => 0,
    "srefs"            => 0,
    "arefs"            => 0,
    "texts"            => 0,
}

# Method

def usage()
    STDERR.printf("%s\n", USAGE_MSG)
end

def cprint(*var)
    console = open('/dev/tty', "w") do |con|
        con.printf(*var)
    end
end

# Parse Options
require 'optparse'
OptionParser.new do |opt|
    opt.banner = USAGE_MSG
    opt.version = MY_VERSION
    opt.on('-l', 'print long format information') { |v| $long = v }
    begin
        opt.parse!(ARGV)
    rescue OptionParser::InvalidOption => e
        puts e.message
        usage
        exit
    end
end

# Open File then Read and Parse
INPUT_FILE = ARGV[0]
if !INPUT_FILE then usage; exit end

begin
    # not neccesary file.close
    File.open(INPUT_FILE, "rb") do |file|
        record = {}
        c = 0
        $bgnstr = 0; $boundary = 0; $path =0;
        $sref = 0; $aref = 0; $text = 0
        # Display Title
        Util.lpr("%4s %3s %-12s %-9s %s\n",
            "#", "byte", "RecType", "DataType",
            "Contents (XY,WIDTH is dbUnit)")
        # Read File
        while !file.eof
            # Get Record Size
            size = file.read(2).unpack("n")[0]    # to big endian
            if size > 0
                data = file.read(size-2)    # get remain record
                record[c] = Record.new(size, data)
                _record_type, _data_type = record[c].to_s
                Util.lpr("%4d: %3d %-12s %-9s",
                    c, size, _record_type, _data_type)
                record[c].parse_data()
                Util.lpr("\n")
            else
                break
            end
            c += 1
            cprint("\r%d records loaded.", c) if !$long
        end
        cprint("%s: %d records, %d structures, %d boundaries, %d paths, %d srefs, %d arefs, %d texts\n",
            INPUT_FILE, c, $log["structures"], $log["boundaries"], $log["paths"], $log["srefs"], $log["arefs"], $log["texts"])
    end
rescue Errno::ENOENT
    STDERR.printf("\n%s not found.\n", INPUT_FILE)
rescue Errno::EACCES
    STDERR.printf("\ncannot open %s.\n", INPUT_FILE)
rescue => e
    STDERR.printf("\nexception: class=#{e.class}, msg=#{e.message}\n")
end

#EOF

■余談

実行してみると、やっぱり遅い。読んでパース、というのを繰り返しているからかもしれません。一気に読んでからパースすれば少しは早くなるかも。でもまあ、速さを求めるなら最初からCで作りますよね。

その昔、まだCGIでもてはやされる前のperl4を勉強したのですが、独特の文法と変数の頭につくいろいろな記号に馴染めないうちに、perl5でがらっと変わってしまい、perlとは決別しました。それ以来、shとawkとCで乗り切ってきました。今回、pythonを横目で見つつrubyに挑戦してみました。

nice!(0)  コメント(0)  トラックバック(0) 
共通テーマ:パソコン・インターネット

nice! 0

コメント 0

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

偏差値rubyをawkのように使うには ブログトップ

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。