
#
# $Id$
#
# = GDSII.rb: interface method for dumping GDSII records 
#
# Author:: Dan White
# 

# == GDSII
#
#  GDSII is a data format used to represent geometric information and
#  is used commonly in electronic design.  The GDSII class currently
#  supports reading a binary GDSII data file and dumping it's contents
#  in a key format ascii file.
#  This module is partially inspired by the perl GDS2 module written
#  by Ken Schumack
#
# == Example
#
#  A simple program to read a GDSII file and dump an ASCII key file:
#
#  require 'GDSII'
#  g = GDS.new("test.gds")
#  while (g.read_record) do
#     puts g.return_str
#  end
#
#

#
# ByteOrder is lifted from ruby-talk 107439, cited by Michael Neumann
#
module ByteOrder 
  Native = :Native
  Big = BigEndian = Network = :BigEndian
  Little = LittleEndian = :LittleEndian

  # examines the locale byte order on the running machine
  def byte_order
    if [0x12345678].pack("L") == "\x12\x34\x56\x78" 
      BigEndian
    else
      LittleEndian
    end
  end
  alias byteorder byte_order
  module_function :byte_order, :byteorder

  def little_endian?
    byte_order == LittleEndian
  end

  def big_endian?
    byte_order == BigEndian
  end

  alias little? little_endian? 
  alias big? big_endian?
  alias network? big_endian?

  module_function :little_endian?, :little?
  module_function :big_endian?, :big?, :network?
end

#
# GDS class
#
class GDS
  @bytesread = 0                # number of bytes read counter
  @gdsfilename = ""             # name of gds file
  @gdsin = nil                  # gds File.open return
  @reclen = 0                   # record length of current record
  @rectype = 0                  # record type of current record
  @datatype = 0                 # data type of current record
  @record = 0                   # raw data record for current record
  @recorddata = 0               # parsed data record for current record

  #
  # Array of GDS record type names
  #
  RecType = [
   "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",        "UINTEGER",
   "USTRING",   "REFLIBS",      "FONTS",        "PATHTYPE",     "GENERATIONS",
   "ATTRTABLE", "STYPTABLE",    "STRTYPE",      "EFLAGS",       "ELKEY", 
   "LINKTYPE",  "LINKKEYS",     "NODETYPE",     "PROPATTR",     "PROPVALUE",
   "BOX",       "BOXTYPE",      "PLEX",         "BGNEXTN",      "ENDEXTN",
   "TAPENUM",   "TAPECODE",     "STRCLASS",     "RESERVED",     "FORMAT",
   "MASK",      "ENDMASKS",     "LIBDIRSIZE",   "SRFNAME",      "LIBSECUR",
   "BORDER",    "SOFTFENCE",    "HARDFENCE",    "SOFTWIRE",     "HARDWIRE",
   "PATHPORT",  "NODEPORT",     "USERCONSTRAINT","SPACERERROR", "CONTACT" ]

  #
  # GDS data types
  #
  NODATA = 0; BITARRAY = 1; INT2 = 2; INT4 = 3; REAL4 = 4; REAL8 = 5;
  ASCII = 6;
  
  #
  # GDS record type values
  #
  HEADER=0;     BGNLIB=1;       LIBNAME=2;      UNITS=3;        ENDLIB=4;
  BGNSTR=5;     STRNAME=6;      ENDSTR=7;       BOUNDARY=8;     PATH=9; 
  SREF=10;      AREF=11;        TEXT=12;        LAYER=13;       DATATYPE=14;
  WIDTH=15;     XY=16;          ENDEL=17;       SNAME=18;       COLROW=19;
  TEXTNODE=20;  NODE=21;        TEXTTYPE=22;    PRESENTATION=23;SPACING=24;
  STRING=25;    STRANS=26;      MAG=27;         ANGLE=28;       UINTEGER=29;
  USTRING=30;   REFLIBS=31;     FONTS=32;       PATHTYPE=33;    GENERATIONS=34;
  ATTRTABLE=35; STYPTABLE=36;   STRTYPE=37;     EFLAGS=38;      ELKEY=39;
  LINKTYPE=40;  LINKKEYS=41;    NODETYPE=42;    PROPATTR=43;    PROPVALUE=44; 
  BOX=45;       BOXTYPE=46;     PLEX=47;        BGNEXTN=48;     ENDEXTN=49;
  TAPENUM=50;   TAPECODE=51;    STRCLASS=52;    RESERVED=53;    FORMAT=54; 
  MASK=55;      ENDMASKS=56;    LIBDIRSIZE=57;  SRFNAME=58;     LIBSECUR=59; 
  BORDER=60;    SOFTFENCE=61;   HARDFENCE=62;   SOFTWIRE=63;    HARDWIRE=64; 
  PATHPORT=65;  NODEPORT=66;    USERCONSTRAINT=67; SPACERERROR=68; CONTACT=69;
  
  #
  # Simple read binary data method 
  #
  def read_data(cnt)
    @bytesread+=cnt
    return @gdsin.read(cnt)
  end
  
  #
  # Read one GDSII record and parse the data into @recorddata based on
  # the record's @dattype
  #
  def read_record
    begin
      #
      # Read the header, first two bytes are length, second two are
      #   record type and data type
      #
      raw = read_data(4)
      @bytesread+=4
      return nil if (raw.nil?)
      reclen_ar = raw[0,2]
      reclen_ar.reverse! if (ByteOrder::little_endian?)
      reclen_ar = reclen_ar.unpack('S')
      @reclen  = reclen_ar[0]
      return nil if (@reclen == 0)
      rectype_ar = raw[2,1]
      rectype_ar = rectype_ar.unpack('c')
      @rectype = rectype_ar[0]
      
      datatype_ar = raw[3,1]
      datatype_ar = datatype_ar.unpack('c')
      @datatype = datatype_ar[0]
      
      bytesleft = @reclen - 4 # reclen includes length of header data, must subtract
      @record = ""
      @recorddata = []
    rescue
      $stderr.puts "Invalid record near #{@bytesread}"
      $stderr.puts "@reclen = #{@reclen}"
      $stderr.puts "@rectype = #{@rectype}"
      $stderr.puts "@datatype = #{@datatype}"
      raise
    end

    
    if (@datatype == BITARRAY)
      raw = read_data(bytesleft)
#      raw.reverse! if (ByteOrder::little_endian?)
      @record += raw
      @recorddata = raw.unpack("B#{bytesleft*8}")
    elsif ((@datatype == INT2) || (@datatype == INT4))
      if (@datatype == INT2)
        numbytes = 2
        utemplate = 's'
      else
        numbytes = 4
        utemplate = 'i'
      end
      while (bytesleft > 0)
        raw = read_data(numbytes)
        raw.reverse! if (ByteOrder::little_endian?)
        @record += raw
        @recorddata.push(raw.unpack(utemplate)[0])
        bytesleft -=numbytes
      end
    elsif (@datatype == REAL4)
      raise "Error: datatype == 4 byte real is unsupported"
    elsif (@datatype == REAL8)
      while (bytesleft>0)
        raw = read_data(1)
        @record += raw
        signval = raw.unpack('B')[0].to_i
        exponent = raw.unpack('C')[0]
        if (signval != 0)
          exponent -= 192
        else
          exponent -= 64
        end
        raw = read_data(7)
        @record += raw
        mant = raw.unpack('b*')[0]
        mantissa = 0.0
        (1 ... 8).each do |i|
          startindx = (i-1)*8
          str = mant[startindx,8]
          ub = [("0"*32+str.reverse.to_s)[-32..-1]].pack("B32").unpack("N")[0]
          mantissa += ub / (256.0 ** i)
        end
        real = mantissa * (16**exponent)
        real = (0-real) if (signval != 0)
        @recorddata = real

        if (@rectype == UNITS)
          if (@usrunits.nil?)
            @usrunits = @recorddata
          elsif (@dbunits.nil?)
            @dbunits = @recorddata
          end
        end
        bytesleft -= 8
      end
    elsif (@datatype == ASCII)
      raw = read_data(bytesleft)
      @record += raw
      @recorddata[0] = raw.unpack("a#{bytesleft}")[0]
      @recorddata[0].gsub!(/\0$/,"")
    end
    return true
  end

  def show_record_and_data
    return @record,@recorddata
  end
  
  def return_str
    str = ""
    indx = 0
    str = RecType[@rectype]+" "

    case (@rectype)
      #
      # NODATA Records
      #
    when ENDLIB,ENDSTR,BOUNDARY,PATH,SREF,AREF,TEXT,
         ENDEL,TEXTNODE,NODE, BOX, RESERVED, ENDMASKS,
         BORDER, SOFTFENCE, HARDFENCE, SOFTWIRE, HARDWIRE,
         PATHPORT, NODEPORT, USERCONSTRAINT, SPACERERROR,
         CONTACT 
      return str

      #
      # INT2 Records
      #
    when HEADER, LAYER, DATATYPE, TEXTTYPE, PATHTYPE,
         GENERATIONS, NODETYPE, PROPATTR, BOXTYPE, PLEX,
         BGNEXTN, ENDEXTN, TAPENUM, TAPECODE, STRCLASS,
         FORMAT, LIBDIRSIZE, LIBSECUR
      @recorddata.each do |chr|
        str += chr.to_s+" "
      end
      return str

      #
      # ASCII Records
      #
    when STRNAME,SNAME, REFLIBS, FONTS,
         ATTRTABLE, PROPVALUE, MASK, SRFNAME 
      return str += @recorddata[0]

    when STRING
      return str += '"'+@recorddata[0]+'"'

    when BGNLIB,BGNSTR
      tmpar = ["LASTMOD","LASTACC"]  if (@rectype == BGNLIB)
      tmpar = ["LASTMOD","CREATION"] if (@rectype == BGNSTR)
      i = 0
      tmpar.each do |label|
        str += sprintf("\n%s %02d/%02d/%02d %02d:%02d:%02d",label,
                       @recorddata[i],@recorddata[i+1],@recorddata[i+2],
                       @recorddata[i+3],@recorddata[i+4],@recorddata[i+5])
        i+=6
      end
      return str

    when LIBNAME
      return str += @recorddata[0]
      
    when UNITS
      return str += "\nUSERUNITS #{@usrunits.to_s}\nPHYSUNITS #{@dbunits.to_s}"

    when WIDTH	# 4 byte INT
      return str += "#{@recorddata[0]*@usrunits}"
      
    when XY
      str += " "+(@recorddata.length/2).to_s+";\n"
      i = 0
      while (i < @recorddata.length)
        str += "  X #{@recorddata[i]*@usrunits}; Y #{@recorddata[i+1]*@usrunits}; \n"
        i += 2
      end
      return str
      
    when COLROW
      @recorddata.each do |chr|
        str += chr.to_s+" "
      end
      return str

    when PRESENTATION
      [@recorddata[0][10,2],
       @recorddata[0][12,2],
       @recorddata[0][14,2]].each do |twobits|
        str += [("0"*32+twobits)[-32..-1]].pack("B32").unpack("N")[0].to_s+","
      end
      str.gsub!(/,$/,"") # strip the final comma
      return str
      
    when STRANS
      [@recorddata[0][0,1],
       @recorddata[0][13,1],
       @recorddata[0][14,1]].each do |onebit|
        str += [("0"*32+onebit)[-32..-1]].pack("B32").unpack("N")[0].to_s+","
      end
      str.gsub!(/,$/,"") # strip the final comma
      return str
      
    when MAG,ANGLE
      return str += @recorddata.to_s

    when SPACING, UINTEGER, USTRING, STYPTABLE, STRTYPE, ELKEY,
         LINKTYPE, LINKKEYS, RESSERVED
      raise "#{RecType[@rectype]} is unsupported"
      
    when EFLAGS
      raise "EFLAGS not implemented"

    end
    raise "Error: Undeclared record type (#{@rectype}), exiting near record #{@bytesread}"
  end
  
  def initialize(gdsfilename)
    @bytesread = 0
    @dbunits = nil
    @usrunits = nil
    @gdsfilename = gdsfilename
    @gdsin = File.new(gdsfilename,"rb")
    @gdsin
  end
end

