#!/usr/bin/ruby -w
# pet.rb -- plain electronics tool (a tiny geda/gschem/gnetlist clone written from scratch in Ruby) 
# This is an early draft or proof of concept -- project name may change
# We try to support recent gschem file format, and to export at least an (extended) PCB netlist
# This file will be supported by a plain Ruby GTK/Cairo schematics editor (name may be peted.rb)
# Copyright: S. Salewski, mail@ssalewski.de
# License: GPL
#
# I am still learning Ruby, so do not expect perfect code!
# Improvements welcome.
#
require 'cairo' # PNG, PDF, SVG export
require 'pango' # font rendering
Version = '0.01 (26-OCT-2010)'
#
Docu = <<HERE
pet.rb -- a plain electronics tool inspired by the gEDA suite
Version: #{Version}
Author: S. Salewski
License: GPL

usage: ...

HERE

PROGRAM_VERSION = 'xxx'
FILEFORMAT_VERSION = 'yyy'
EGRID = 100 # pins and nets should start/end on EGRID only
MIN_STRUCTURE_SIZE = 5 # rectangles, circles should not be smaller
MAX_EXTEND = 1e6.to_i # -MAX_EXTEND < user coordinates < MAX_EXTEND
COLOR_INDEX_RANGE = 0...24
DefaultSymDirs = ['/usr/share/gEDA/sym', 'sym', '/home']

# line pattern
GEDA_TYPE_SOLID   = 0 # -----
GEDA_TYPE_DOTTED  = 1 # . . . 
GEDA_TYPE_DASHED  = 2 # - - -
GEDA_TYPE_CENTER  = 3 # - . - .
GEDA_TYPE_PHANTOM = 4 # - . . - ..
GEDA_SUPPORTED_LINE_PATTERNS = GEDA_TYPE_SOLID..GEDA_TYPE_PHANTOM
# line end/join
GEDA_END_NONE   = 0
GEDA_END_SQUARE = 1
GEDA_END_ROUND  = 2
GEDA_SUPPORTED_LINE_ENDS = GEDA_END_NONE..GEDA_END_ROUND
# filling
GEDA_FILLING_HOLLOW = 0
GEDA_FILLING_FILL   = 1
GEDA_FILLING_MESH   = 2
GEDA_FILLING_HATCH  = 3
#GEDA_FILLING_VOID  = 4 # unused
GEDA_SUPPORTED_FILLS = GEDA_FILLING_HOLLOW..GEDA_FILLING_HATCH

ArcChar     = 'A'
BoxChar     = 'B'
BusChar     = 'U'
CircChar    = 'V'
LineChar    = 'L'
NetSegChar  = 'N'
PathChar    = 'H'
PicChar     = 'G'
PinChar     = 'P'
SymChar     = 'C'
TextChar    = 'T'
VersionChar = 'v'

AttrPat = '(.*)=(.*)'

SOLPAT   = '^'
EOLPAT   = '$'
INTPAT   = ' (-?\d+)'
FLOATPAT = ' (\d+(\.\d+)?([eE][-+]?\d+)?)'

ArcPar = %w[Arc type x y radius startangle sweepangle color width capstyle dashstyle dashlength dashspace]
ArcPat = SOLPAT + '(' + ArcChar + ')' + INTPAT * 11 + EOLPAT

BoxPar = %w[Box type x y width height color linewidth capstyle dashstyle dashlength dashspace filltype fillwidth angle1 pitch1 angle2 pitch2]
BoxPat = SOLPAT + '(' + BoxChar + ')' + INTPAT * 16 + EOLPAT

BusPar = %w[Bus type x1 y1 x2 y2 color ripperdir]
BusPat = SOLPAT + '(' + BusChar + ')' + INTPAT * 6 + EOLPAT

CircPar = %w[Circ type x y radius color width capstyle dashstyle dashlength dashspace filltype fillwidth angle1 pitch1 angle2 pitch2]
CircPat = SOLPAT + '(' + CircChar + ')' + INTPAT * 15 + EOLPAT

SymPar = %w[Symbol type x y selectable angle mirror basename]
EmbeddedSymPat = SOLPAT + '(' + SymChar + ')' + INTPAT * 5 + ' EMBEDDED' + '(.*\.sym)' + EOLPAT
ExternSymPat   = SOLPAT + '(' + SymChar + ')' + INTPAT * 5 + ' '         + '(.*\.sym)' + EOLPAT

LinePar = %w[Line type x1 y1 x2 y2 color width capstyle dashstyle dashlength dashspace]
LinePat = SOLPAT + '(' + LineChar + ')' + INTPAT * 10 + EOLPAT

PathPar = %w[Path type color width capstyle dashstyle dashlength dashspace filltype fillwidth angle1 pitch1 angle2 pitch2 numlines]
PathPat = SOLPAT + '(' + PathChar + ')' + INTPAT * 13 + EOLPAT

PicPar = %w[Pic type x y width height angle ratio mirrored embedded]
PicPat = SOLPAT + '(' + PicChar + ')' + INTPAT * 5 + FLOATPAT + INTPAT * 2 + EOLPAT

PinPar = %w[Pin type x1 y1 x2 y2 color pintype whichend]
PinPat = SOLPAT + '(' + PinChar + ')' + INTPAT * 7 + EOLPAT

TextPar = %w[Text type x y color size visibility show_name_value angle alignment num_lines]
TextPat = SOLPAT + '(' + TextChar + ')' + INTPAT * 9 + EOLPAT

NetSegPar = %w[NetSeg type x1 y1 x2 y2 color]
NetSegPat = SOLPAT + '(' + NetSegChar + ')' + INTPAT * 5 + EOLPAT

VersionPar = %w[Version type version fileformat_version]
VersionPat = SOLPAT + '(' + VersionChar + ')' + INTPAT * 2 + EOLPAT

=begin
# plain source code generation -- save some typing effort
def ppar(a)
  a.each_with_index{|x,i| print "  el.#{x} = match[#{i}]\n"}
  print '  attr_accessor'; a.each{|x| print ' :', x, ','}; puts
  print '  pline'; a.each{|x| print ' @', x, ','}; puts
  puts
end

ppar(ArcPar)
ppar(BoxPar)
ppar(BusPar)
ppar(CircPar)
ppar(LinePar)
ppar(NetSegPar)
ppar(PathPar)
ppar(PicPar)
ppar(PinPar)
ppar(SymPar)
ppar(TextPar)
ppar(VersionPar)
Process.exit
=end

Default_Line_Width_Scale = 15 # compare to EGRID==100, PIN-Length==300
Default_Text_Size_Scale = 15

class Cairo::Context
  attr_accessor :sharp_lines, :line_width_unscaled_user_add_on, :line_width_device_min, :line_width_unscaled_user_min, :line_width_scale
  alias :orig_init :initialize
  def initialize(surface)
    orig_init(surface)
    @sharp_lines = true # defaults, overwrite for each surface if necessary
    @line_width_device_min = 1
    @line_width_unscaled_user_min = 1
    @line_width_scale = Default_Line_Width_Scale
  end

  # ensure minimum width for user and device space, and scale in relation to egrid
  if method_defined?(:unscaled_user_to_device_line_width)
    puts 'We are unintentionally overwriting method Cairo::Context.unscaled_user_to_device_line_width'
    Process.exit
  else
    def unscaled_user_to_device_line_width(u)
      x = [u, @line_width_unscaled_user_min].max * @line_width_scale
      return [self.user_to_device_distance(x, 1)[0].abs, @line_width_device_min].max
    end
  end

  if method_defined?(:fixed_set_line_width)
    puts 'We are unintentionally overwriting method Cairo::Context.fixed_set_line_width'
    Process.exit
  else
    def fixed_set_line_width(w)
      self.set_line_width(self.device_to_user_distance(self.unscaled_user_to_device_line_width(w), 1)[0].abs)
    end
  end

  if method_defined?(:sharp_line)
    puts 'We are unintentionally overwriting method Cairo::Context.sharp_line'
    Process.exit
  else
=begin
    def sharp_line(x1, y1, x2, y2, wu)
      if @sharp_lines
        eps = 0.1
        wd = self.user_to_device_distance(wu, 1)[0]
        wd = (wd + @line_width_add_on).to_i
        wu = self.device_to_user_distance(wd, 1)[0]
        x1, y1 = self.user_to_device(x1, y1)
        x2, y2 = self.user_to_device(x2, y2)
        snap = false
        if (x1 - x2).abs < eps
          x1 = (x1 + x2) * 0.5
          x2 = x1
          snap = true
        elsif (y1 - y2).abs < eps
          y1 = (y1 + y2) * 0.5
          y2 = y1
          snap = true
        end
        if snap
          if wd % 2 == 0
            x1 = x1.round
            y1 = y1.round
            x2 = x2.round
            y2 = y2.round
          else
            x1 = x1.to_i + 0.5
            y1 = y1.to_i + 0.5
            x2 = x2.to_i + 0.5
            y2 = y2.to_i + 0.5
          end
        end
        x1, y1 = self.device_to_user(x1, y1)
        x2, y2 = self.device_to_user(x2, y2)
      end
      self.set_line_width(wu)
      self.move_to(x1, y1)
      self.line_to(x2, y2)
      self.stroke
    end
=end
#=begin
    def sharp_line(x1, y1, x2, y2, wu)
      aa = self.antialias
      if @sharp_lines
        eps = 0.1
        xd1, yd1 = self.user_to_device(x1, y1)
        xd2, yd2 = self.user_to_device(x2, y2)
        if (((xd1 - xd2).abs < eps) or ((yd1 - yd2).abs < eps))
          self.set_antialias(Cairo::ANTIALIAS_NONE)
        end
      end
      self.fixed_set_line_width(wu)
      self.move_to(x1, y1)
      self.line_to(x2, y2)
      self.stroke
      self.set_antialias(aa)
    end
#=end
  end
  if method_defined?(:sharp_rect)
    puts 'We are unintentionally overwriting method Cairo::Context.sharp_rect'
    Process.exit
  else
    def sharp_rect(x, y, w, h, wu)
      aa = self.antialias
      if @sharp_lines
        self.set_antialias(Cairo::ANTIALIAS_NONE)
      end
      self.fixed_set_line_width(wu)
      self.rectangle(x, y, w, h)
      self.stroke
      self.set_antialias(aa)
    end
  end
end

class BBOX
  attr_accessor :x1, :y1, :x2, :y2
  def initialize
    @x1, @y1, @x2, @y2 = MAX_EXTEND, MAX_EXTEND, -MAX_EXTEND, -MAX_EXTEND
  end
end

def on_egrid?(*p)
  p.each{|x| if x % EGRID != 0 then return false end}
  return true
end

def diagonal_line?(x1, y1, x2, y2)
  (x1 != x2) and (y1 != y2)
end

def line_too_short?(x1, y1, x2, y2)
  ((x1 - x2) ** 2 + (y1 - y2) ** 2) < 1
end

class Element
  attr_reader :type
  attr_reader :bx1, :by1, :bx2, :by2 # bounding box
  # state (visible, selectable, selected, deleted, ...)
  # parent (we may need this to delete elements)
  def check; return ''; end
  def mytos(*a)
    a.join(' ') + "\n"
  end
  def to_s
    mytos @type
  end
  def write(file)
    file.write to_s
  end
  def set_box(); end
  def translate(x, y); end
  def rotate(x, y, angle); end
  def enlarge_bbox(b)
    if defined? @bx1 # remove later!
      if b.x1 > @bx1 then b.x1 = @bx1 end
      if b.x2 < @bx2 then b.x2 = @bx2 end
      if b.y1 > @by1 then b.y1 = @by1 end
      if b.y2 < @by2 then b.y2 = @by2 end
    end
  end
  def query_hit(hitlist); end
end

class Versio < Element
  attr_accessor :version, :fileformat_version
  def initialize
    @type = VersionChar
    @version = PROGRAM_VERSION
    @fileformat_version = FILEFORMAT_VERSION
  end
  def to_s
    mytos @type, @version, @fileformat_version
  end
  def write(file)
    file.write to_s
  end
  def draw(cr, cvalue, par); end
  def enlarge_bbox(b); end
end

def set_dash(cr, dashstyle, dashlength, dashspace)
  if dashstyle == GEDA_TYPE_SOLID
    cr.set_dash([], 0)
  elsif dashstyle == GEDA_TYPE_DOTTED
    cr.set_line_cap(Cairo::LINE_CAP_ROUND)
    cr.set_dash([0, dashspace], 0)
  elsif dashstyle == GEDA_TYPE_DASHED
    cr.set_dash([dashlength, dashspace], dashlength * 0.5)
  elsif dashstyle == GEDA_TYPE_CENTER
    cr.set_line_cap(Cairo::LINE_CAP_ROUND)
    cr.set_dash([dashlength, dashspace, 0, dashspace], dashlength * 0.5)
  elsif dashstyle == GEDA_TYPE_PHANTOM
    cr.set_line_cap(Cairo::LINE_CAP_ROUND)
    cr.set_dash([dashlength, dashspace, 0, dashspace, 0, dashspace], dashlength * 0.5)
  end
end

def set_cap(cr, cap)
  if cap == GEDA_END_NONE
    cr.set_line_cap(Cairo::LINE_CAP_BUTT)
  elsif cap == GEDA_END_SQUARE
    cr.set_line_cap(Cairo::LINE_CAP_SQUARE)
  elsif cap == GEDA_END_ROUND
    cr.set_line_cap(Cairo::LINE_CAP_ROUND)
  end
end

def set_join(cr, cap)
  if cap == GEDA_END_NONE
    cr.set_line_join(Cairo::LINE_JOIN_BEVEL)
  elsif cap == GEDA_END_SQUARE
    cr.set_line_join(Cairo::LINE_JOIN_MITER)
  elsif cap == GEDA_END_ROUND
    cr.set_line_join(Cairo::LINE_JOIN_ROUND)
  end
end

class Line < Element
  attr_accessor :x1, :y1, :x2, :y2, :color, :width, :capstyle, :dashstyle, :dashlength, :dashspace
  def initialize
    @type = LineChar
    @x1, @y1, @x2, @y2, @color, @width = 0, 0, 0, 0, 0, 0 # overwrite, or check() will complain
    @capstyle = GEDA_END_ROUND
    @dashstyle = GEDA_TYPE_SOLID
    @dashlength = -1
    @dashspace = -1
  end
  def check
    if @width < 0
      return "line width should not be negative (#{@width})\n"
    elsif not GEDA_SUPPORTED_LINE_ENDS.include? @capstyle
      return "unsupported cap style (#{@capstyle})\n"
    elsif not GEDA_SUPPORTED_LINE_PATTERNS.include? @dashstyle
      return "unsupported dash style (#{@dashstyle})\n"
    elsif (@dashstyle != GEDA_TYPE_SOLID) and (@dashspace <= 0)
      return "dashspace should be >= 0 (#{@dashspace})\n" # we should query result of cairo stroke for other problems 
    elsif not COLOR_INDEX_RANGE.include? @color
      return "color index #{@color} out of range\n" 
    elsif line_too_short?(@x1, @y1, @x2, @y2)
      return "very short line, #{@x1}, #{@y1} -- #{@x2}, #{@y2}\n"
    else
      return ''
    end
  end
  def to_s
    mytos @type, @x1, @y1, @x2, @y2, @color, @width, @capstyle, @dashstyle, @dashlength, @dashspace
  end
  def write(file)
    file.write to_s
  end
  def set_box()
    @bx1, @bx2 = [@x1, @x2].minmax
    @by1, @by2 = [@y1, @y2].minmax
  end
  def translate(x, y)
    @x1 += x; @x2 += x; @y1 += y ;@y2 += y;
    @bx1 += x; @bx2 += x; @by1 += y ;@by2 += y;
  end
  def draw(cr, cvalue, par)
    cr.set_source_rgb(*cvalue[@color])
    set_cap(cr, capstyle)
    set_dash(cr, dashstyle, dashlength, dashspace)
    cr.sharp_line(x1, y1, x2, y2, @width)
  end
end

class NetSegment < Element
  attr_accessor :x1, :y1, :x2, :y2, :color
  def initialize
    @type = NetSegChar
    @x1, @y1, @x2, @y2 = 0, 0, 0, 0 # overwrite, or check() will complain
    @color = NET_COLOR
  end
  def check
    if not COLOR_INDEX_RANGE.include? @color
      return "color index #{@color} out of range\n"
    elsif @color != NET_COLOR
      return " color index (#{@color}) should be #{NET_COLOR}\n" # warning
    elsif diagonal_line?(@x1, @y1, @x2, @y2)
      t = 'diagonal net'
    elsif not on_egrid?(@x1, @y1, @x2, @y2) 
      t = 'net segment is not on e-grid'
    elsif ((@x1 == @x2) and (@y1 == @y2))
      t = 'net segment of length 0'
    else
      t = ''
    end
    t <<  ", #{@x1}, #{@y1} -- #{@x2}, #{@y2}\n" unless t == ''
    return t
  end
  def to_s
    mytos @type, @x1, @y1, @x2, @y2, @color
  end
  def write(file)
    file.write to_s
  end
  def set_box()
    @bx1, @bx2 = [@x1, @x2].minmax
    @by1, @by2 = [@y1, @y2].minmax
  end
  def translate(x, y)
    @x1 += x; @x2 += x; @y1 += y ;@y2 += y;
    @bx1 += x; @bx2 += x; @by1 += y ;@by2 += y;
  end
  def draw(cr, cvalue, par)
    set_cap(cr, par[:net_end_cap])
    cr.set_source_rgb(*cvalue[@color])
    cr.sharp_line(x1, y1, x2, y2, par[:line_width_net])
  end
end

class Box < Element
  attr_accessor :x, :y, :width, :height, :color, :linewidth, :capstyle, :dashstyle, :dashlength, :dashspace, :filltype, :fillwidth, :angle1, :pitch1, :angle2, :pitch2
  def initialize
    @type = BoxChar
    @x, @y, @width, @height, @color, @linewidth = 0, 0, 0, 0, 0, 0 # overwrite, or check() will complain
    @capstyle = GEDA_END_ROUND
    @dashstyle = GEDA_TYPE_SOLID
    @dashlength = -1
    @dashspace = -1
    @filltype = GEDA_FILLING_HOLLOW
    @fillwidth = -1
    @angle1 = -1
    @pitch1 = -1
    @angle2 = -1
    @pitch2 = -1
  end
  def check
    if @linewidth < 0
      return "line width should not be negative (#{@linewidth})\n"
    elsif (@filltype == GEDA_FILLING_MESH) or (@filltype == GEDA_FILLING_HATCH)
      if @pitch1 <= 0
        return "pitch1 for filling should be > 0(#{@pitch1})\n"
      elsif not (-360..360).include? @angle1
        return "angle1 for filling should be in the range -360..360 (#{@angle1})\n"
      end
      if @filltype == GEDA_FILLING_MESH
        if @pitch2 <= 0
          return "pitch2 for filling should be > 0(#{@pitch2})\n"
        elsif not (-360..360).include? @angle2
          return "angle2 for filling should be in the range -360..360 (#{@angle2})\n"
        elsif @angle1 == @angle2
          return " mesh with angle1 == angle2 (#{@angle1})\n" # warning, may be intended for different pitch
        end
      end
    elsif not GEDA_SUPPORTED_LINE_ENDS.include? @capstyle
      return "unsupported cap style (#{@capstyle})\n"
    elsif not GEDA_SUPPORTED_LINE_PATTERNS.include? @dashstyle
      return "unsupported dash style (#{@dashstyle})\n"
    elsif (@dashstyle != GEDA_TYPE_SOLID) and (@dashspace <= 0)
      return "dashspace should be >= 0 (#{dashspace})\n" # we should query result of cairo stroke for other problems 
    elsif not COLOR_INDEX_RANGE.include? @color
      return "color index #{@color} out of range\n" 
    elsif [@width, @height].min < MIN_STRUCTURE_SIZE # should we support negative @width, @height?
      return "very small rectangle (#{@width}, #{@height})\n"
    else
      return ''
    end
  end
  def to_s
    mytos @type, @x, @y, @width, @height, @color, @linewidth, @capstyle, @dashstyle, @dashlength, @dashspace, @filltype, @fillwidth, @angle1, @pitch1, @angle2, @pitch2
  end
  def write(file)
    file.write to_s
  end
  def set_box()
    @bx1, @bx2 = [@x, @x + @width].minmax # works for neg. @width, @height
    @by1, @by2 = [@y, @y + @height].minmax
  end
  def translate(x, y)
    @x += x; @y += y;
    @bx1 += x; @bx2 += x; @by1 += y ;@by2 += y;
  end
  def draw(cr, cvalue, par)
    cr.set_source_rgb(*cvalue[@color])
    if (@filltype == GEDA_FILLING_MESH) or (@filltype == GEDA_FILLING_HATCH)
      cr.save
      cr.rectangle(@x, @y, @width, @height)
      #cr.clip
      cr.translate(@x + @width * 0.5, @y + @height * 0.5)
      cr.set_dash([], 0)
      cr.fixed_set_line_width(@fillwidth)
      z = Math::sqrt(@width ** 2 + @height ** 2) * 0.5 
      cr.rotate(@angle1 / 180.0 * Math::PI)
      p = - z
      while p < z do
        cr.move_to(-z, p)
        cr.line_to(z, p)
        p += @pitch1
      end
      cr.stroke
      if (@filltype == GEDA_FILLING_MESH)
        cr.rotate((@angle2 - @angle1) / 180.0 * Math::PI)
        p = - z
        while p < z do
          cr.move_to(-z, p)
          cr.line_to(z, p)
          p += @pitch2
        end
        cr.stroke
      end
      cr.restore
    end
    if (@filltype == GEDA_FILLING_FILL) # we have no separate fill color
      cr.rectangle(@x, @y, @width, @height)
      cr.fill
    end
    set_join(cr, @capstyle)
    set_dash(cr, @dashstyle, @dashlength, @dashspace)
    cr.sharp_rect(@x, @y, @width, @height, @linewidth)
  end
end

class Circ < Element
  attr_accessor :x, :y, :radius, :color, :width, :capstyle, :dashstyle, :dashlength, :dashspace, :filltype, :fillwidth, :angle1, :pitch1, :angle2, :pitch2
  def initialize
    @type = CircChar
    @x, @y, @radius, @color, @width = 0, 0, 0, 0, 0 # overwrite, or check() will complain
    @capstyle = GEDA_END_ROUND
    @dashstyle = GEDA_TYPE_SOLID
    @dashlength = -1
    @dashspace = -1
    @filltype = GEDA_FILLING_HOLLOW
    @fillwidth = -1
    @angle1 = -1
    @pitch1 = -1
    @angle2 = -1
    @pitch2 = -1
  end
  def check
    if @linewidth < 0
      return "line width should not be negative (#{@linewidth})\n"
    elsif (@filltype == GEDA_FILLING_MESH) or (@filltype == GEDA_FILLING_HATCH)
      if @pitch1 <= 0
        return "pitch1 for filling should be > 0(#{@pitch1})\n"
      elsif not (-360..360).include? @angle1
        return "angle1 for filling should be in the range -360..360 (#{@angle1})\n"
      end
      if @filltype == GEDA_FILLING_MESH
        if @pitch2 <= 0
          return "pitch2 for filling should be > 0(#{@pitch2})\n"
        elsif not (-360..360).include? @angle2
          return "angle2 for filling should be in the range -360..360 (#{@angle2})\n"
        elsif @angle1 == @angle2
          return " mesh with angle1 == angle2 (#{@angle1})\n" # warning, may be intended for different pitch
        end
      end
    elsif not GEDA_SUPPORTED_LINE_ENDS.include? @capstyle
      return "unsupported cap style (#{@capstyle})\n"
    elsif not GEDA_SUPPORTED_LINE_PATTERNS.include? @dashstyle
      return "unsupported dash style (#{@dashstyle})\n"
    elsif (@dashstyle != GEDA_TYPE_SOLID) and (@dashspace <= 0)
      return "dashspace should be >= 0 (#{dashspace})\n" # we should query result of cairo stroke for other problems 
    elsif not COLOR_INDEX_RANGE.include? @color
      return "color index #{@color} out of range\n" 
    elsif @radius < MIN_STRUCTURE_SIZE
      return "very small circle (#{@radius})\n"
    else
      return ''
    end
  end
  def to_s
    mytos @type, @x, @y , @radius, @color, @width, @capstyle, @dashstyle, @dashlength, @dashspace, @filltype, @fillwidth, @angle1, @pitch1, @angle2, @pitch2
  end
  def write(file)
    file.write to_s
  end
  def set_box()
    @bx1, @bx2 = @x - @radius, @x + @radius
    @by1, @by2 = @y - @radius, @y + @radius
  end
  def translate(x, y)
    @x += x; @y += y
    @bx1 += x; @bx2 += x; @by1 += y ;@by2 += y;
  end
  def draw(cr, cvalue, par)
    cr.set_source_rgb(*cvalue[@color])
    if (@filltype == GEDA_FILLING_MESH) or (@filltype == GEDA_FILLING_HATCH)
      cr.save
      cr.new_sub_path
      cr.arc(@x, @y, @radius, 0, 2 * Math::PI)
      #cr.clip
      cr.translate(@x, @y)
      cr.set_dash([], 0)
      cr.fixed_set_line_width(@fillwidth)
      z = @radius 
      cr.rotate(@angle1 / 180.0 * Math::PI)
      p = - z
      while p < z do
        cr.move_to(-z, p)
        cr.line_to(z, p)
        p += @pitch1
      end
      cr.stroke
      if (@filltype == GEDA_FILLING_MESH)
        cr.rotate((@angle2 - @angle1) / 180.0 * Math::PI)
        p = - z
        while p < z do
          cr.move_to(-z, p)
          cr.line_to(z, p)
          p += @pitch2
        end
        cr.stroke
      end
      cr.restore
    end
    set_join(cr, @capstyle)
    set_dash(cr, @dashstyle, @dashlength, @dashspace)
    cr.fixed_set_line_width(@width)
    cr.new_sub_path
    cr.arc(@x, @y, @radius, 0, 2 * Math::PI)
    if (@filltype == GEDA_FILLING_FILL) # we have no separate fill color
      cr.fill_preserve
    end
    cr.stroke
  end
end

class Arc < Element
end

class Path < Element
end

GEDA_TEXT_INVISIBLE = 0
GEDA_TEXT_VISIBLE = 1
GEDA_TEXT_VIS_RANGE = GEDA_TEXT_INVISIBLE..GEDA_TEXT_VISIBLE
GEDA_TEXT_SHOW_NAME_VALUE = 0
GEDA_TEXT_SHOW_VALUE = 1
GEDA_TEXT_SHOW_NAME = 2
GEDA_TEXT_SHOW_RANGE = GEDA_TEXT_SHOW_NAME_VALUE..GEDA_TEXT_SHOW_NAME
TEXT_SIZE_DEFAULT = 10
TEXT_SIZE_MIN = 2
GEDA_TEXT_ALIGNMENT_RANGE = 0..8
# alignment
# 2 5 8
# 1 4 7
# 0 3 6
# x = -(a / 3) * 0.5 # origin transformation: cairo/pango top/left to gschem's alignment 
# y = -1 + (a % 3) * 0.5
# Attribute (name = value) or plain text
class Text < Element
  attr_accessor :x, :y, :color, :size, :visibility, :show_name_value, :angle, :alignment, :num_lines
  attr_accessor :lines
  def initialize
    @type = TextChar
    @x, @y = 0, 0
    @color = TEXT_COLOR
    @size = TEXT_SIZE_DEFAULT
    @visibility = GEDA_TEXT_VISIBLE
    @show_name_value = GEDA_TEXT_SHOW_VALUE
    @angle = 0
    @alignment = 0
    @num_lines = 0
    @lines = Array.new
  end
  def check
    if not COLOR_INDEX_RANGE.include? @color
      return "color index #{@color} out of range\n"
    elsif @size < TEXT_SIZE_MIN
      return "tiny text (#{@size})\n"
    elsif not GEDA_TEXT_SHOW_RANGE.include? @show_name_value
      return "show name/value is invalid (#{@show_name_value})\n"
    elsif not GEDA_TEXT_VIS_RANGE.include? @visibility
      return "visibility is a boolean 0/1 (#{@visibility})\n"
    elsif not GEDA_TEXT_ALIGNMENT_RANGE.include? @alignment
      return "alignment range is 0..8 (#{@alignment})\n"
    elsif not (-360..360).include? @angle
      return "angle should be in the range -360..360 (#{@angle})\n"
    elsif @num_lines <= 0 
      return "num lines should be > 0 (#{@num_lines})\n"
    elsif @num_lines != @lines.length 
      return "num lines (#{@num_lines}) != length of array (#{@lines.length})\n"
    else
      return ''
    end
  end
  def to_s
    mytos(@type, @x, @y, @color, @size, @visibility, @show_name_value, @angle, @alignment, @num_lines) + @lines.join("\n") + "\n"
  end
  def write(file)
    file.write to_s
  end
#  def set_box()
#    @bx1, @bx2 = [@x1, @x2].minmax
#    @by1, @by2 = [@y1, @y2].minmax
#  end
  def translate(x, y)
    @x += x; @y += y
    #@bx1 += x; @bx2 += x; @by1 += y; @by2 += y;
  end
  # translate to xy, fix alignment/origin and rotate around origin -- rotate will not change alignment/origin marker (x)
  def draw(cr, cvalue, par)
    if (@num_lines <= 0) or (@lines.length == 0) or (@visibility == GEDA_TEXT_INVISIBLE) then return end
    if @show_name_value == GEDA_TEXT_SHOW_NAME_VALUE
      d = @lines
    else
      d = Array.new
      @lines.each do |l| # each line ends with \n
        n, v = l.split('=')
        if v == nil
          d.push l
        elsif @show_name_value == GEDA_TEXT_SHOW_VALUE
          d.push v
        else
          d.push(n + "\n")
        end
      end
    end
    t = d.join
    overline = false
    l = t.length - 1 # last char is \n
    i, j = 0, 0
    tt = String.new
    o = Array.new
    while i < l do
      if (t[i, 1] == '\\') and (t[i + 1, 1] == '_')
        overline = (not overline)
        i += 2
      else
        tt << t[i]
        if overline
          o.push j
        end
        i += 1
        j += 1
      end
    end

    cr.save
    cr.translate(@x, @y) #first suppose our text origin is upper left corner (alignment==2
cr.rotate(@angle * Math::PI / 180.0)
    d = par[:text_mark_size] * 0.5
#d = 200.0
    if par[:text_mark_visible_always] and (d > 0)
      cr.fixed_set_line_width(par[:text_mark_width])
#cr.set_line_width(7)
      cr.set_source_rgb(*cvalue[TEXT_MARK_COLOR])
      cr.set_line_cap(Cairo::LINE_CAP_ROUND)
      cr.set_dash([], 0)
      cr.move_to(-d, -d)
      cr.line_to(d, d)
      cr.move_to(-d, d)
      cr.line_to(d, -d)
      cr.stroke
    end
    cr.scale(1, -1) # reset our mirrored y axis, now we have the default again
    layout = cr.create_pango_layout
    layout.set_text(tt)
    desc = Pango::FontDescription.new(par[:text_font_name])
    desc.set_size(@size * par[:text_size_system_scale] * par[:text_size_user_scale] * Pango::SCALE)
    layout.set_font_description(desc)
    unless par[:text_field_transparent]
      al = Pango::AttrList.new
      background_attr = Pango::AttrBackground.new(*cvalue[BACKGROUND_COLOR].map{|x| x * (2**16 - 1)})
      background_attr.start_index = 0 # Pango::ATTR_INDEX_FROM_TEXT_BEGINNING # Since: 1.24
      background_attr.end_index = -1 # Pango::ATTR_INDEX_TO_TEXT_END # Since: 1.24
      al.insert(background_attr)
      layout.set_attributes(al)
    end
    cr.update_pango_layout(layout)
    inc_rect, log_rect = layout.extents
    cr.translate((-(@alignment / 3) * 0.5) * log_rect.width / Pango::SCALE, (-1 + (@alignment % 3) * 0.5) * log_rect.height / Pango::SCALE)
    #cr.rotate(@angle * Math::PI / 180)
    cr.move_to(0, 0)
    cr.show_pango_layout(layout)  
    # cr.rectangle(0, 0, log_rect.width / Pango::SCALE, log_rect.height / Pango::SCALE)

    if not o.empty?
    pos = Pango::Rectangle.new(0,0,0,0)

    pos = layout.index_to_pos(o[0])
    cr.move_to(pos.x/Pango::SCALE, pos.y/Pango::SCALE)
    cr.line_to((pos.x+pos.width)/Pango::SCALE, (pos.y)/Pango::SCALE)
    cr.stroke
    end

    cr.restore
  end

end

class Pin < Element
  attr_accessor :x1, :y1, :x2, :y2, :color, :pintype, :whichend
  attr_accessor :attributes
  def initialize
    @type = PinChar
    @color = PIN_COLOR
    @attributes = Array.new
  end
  def attr_to_s
    if attributes.empty? then '' else "{\n" + @attributes.join("\n") + "}\n" end
  end
  def to_s
    mytos(@type, @x1, @y1, @x2, @y2, @color, @pintype, @whichend) + attr_to_s
  end
  def write(file)
    file.write to_s
  end
  def set_box()
    @bx1, @bx2 = [@x1, @x2].minmax
    @by1, @by2 = [@y1, @y2].minmax
  end
  def translate(x, y)
    @x1 += x; @x2 += x; @y1 += y ;@y2 += y;
    @bx1 += x; @bx2 += x; @by1 += y ;@by2 += y;
  end
  def draw(cr, cvalue, par)
    cr.set_source_rgb(*cvalue[@color]) # @color should be PIN_COLOR
    cr.move_to(x1, y1); cr.line_to(x2, y2)
    cr.set_line_width(par[:line_width_net] * par[:line_width_scale] * par[:lws])
    cr.stroke
    @attributes.each{|x| x.draw(cr, cvalue, par)  unless x == nil}
  end
end

class EmbSym < Element
  attr_accessor :x, :y, :selectable, :angle, :mirror, :basename
  attr_accessor :components # pins, attributes and graphical elements
  attr_accessor :attributes 
  def initialize
    @type = SymChar
    @components = Array.new
    @attributes = Array.new
  end  
  def comp_to_s
    if @components.empty? then '' else "[\n" + @components.join("\n") + "]\n" end
  end
  def attr_to_s
   if @attributes.empty? then '' else "{\n" + @attributes.join("\n") + "}\n" end
  end
  def to_s
    mytos(@type, @x, @y, @selectable, @angle, @mirror, @basename) + comp_to_s + attr_to_s
  end
  def write(file)
    if self.class == EmbSym
      file.write to_s
    else
      file.write attr_to_s()
    end
  end

  def set_box()
    @components.each{|x| x.set_box unless x == nil}
    @attributes.each{|x| x.set_box unless x == nil}
    b = BBOX.new
    @components.each{|x| x.enlarge_bbox(b) unless x == nil}
    @attributes.each{|x| x.enlarge_bbox(b) unless x == nil}
    @bx1, @by1, @bx2, @by2, = b.x1, b.y1, b.x2, b.y2
  end



  def translate(x, y)
    @x += x; @y += y
    @bx1 += x; @bx2 += x; @by1 += y ;@by2 += y;
  end


  def draw(cr, cvalue, par)
    @components.each{|x| x.draw(cr, cvalue, par) unless x == nil}
    @attributes.each{|x| x.draw(cr, cvalue, par)  unless x == nil}
  end
  def translate(x, y)
    @x += x
    @y += y
    #@components.each{|x| x.translate(x, y)}
    #@attributes.each{|x| x.translate(x, y)}
  end
end

class Sym < EmbSym
  def draw(cr, cvalue, par)
    cr.save
#    cr.translate(@x, @y)
    super(cr, cvalue, par)
    cr.restore
  end
end

# special character indicating end of file
# NewLine() will set @ThisLine[0] to this character if end of file is reached
EOFC = ['~']

CoordinateRange = 1_000_000

# gschems color indizes
  ColIndex = {
	:BACKGROUND_COLOR         => 0,
	:PIN_COLOR                => 1,
	:NET_ENDPOINT_COLOR       => 2,
	:GRAPHIC_COLOR            => 3,
	:NET_COLOR                => 4,
	:ATTRIBUTE_COLOR          => 5,
	:LOGIC_BUBBLE_COLOR       => 6,
	:DOTS_GRID_COLOR          => 7,
	:DETACHED_ATTRIBUTE_COLOR => 8,
	:TEXT_COLOR               => 9,
	:BUS_COLOR                => 10,
	:SELECT_COLOR             => 11,
	:BOUNDINGBOX_COLOR        => 12,
	:ZOOM_BOX_COLOR           => 13,
	:STROKE_COLOR             => 14,
	:LOCK_COLOR               => 15,
	:OUTPUT_BACKGROUND_COLOR  => 16,
	:FREESTYLE1_COLOR         => 17,
	:FREESTYLE2_COLOR         => 18,
	:FREESTYLE3_COLOR         => 19,
	:FREESTYLE4_COLOR         => 20,
	:JUNCTION_COLOR           => 21,
	:MESH_GRID_MAJOR_COLOR    => 22,
	:MESH_GRID_MINOR_COLOR    => 23}

  DefColVal = Array.new(24)
  DefColVal[BACKGROUND_COLOR         = 0 ]  = [0.7, 0.7, 0.7] # black
  DefColVal[PIN_COLOR                = 1 ]  = [1, 1, 1] # white
  DefColVal[NET_ENDPOINT_COLOR       = 2 ]  = [0, 0, 0] # black
  DefColVal[GRAPHIC_COLOR            = 3 ]  = [0, 0, 0] # black
  DefColVal[NET_COLOR                = 4 ]  = [0, 1, 0] # green
  DefColVal[ATTRIBUTE_COLOR          = 5 ]  = [0, 0, 0] # black
  DefColVal[LOGIC_BUBBLE_COLOR       = 6 ]  = [0, 0, 0] # black
  DefColVal[DOTS_GRID_COLOR          = 7 ]  = [0, 0, 0] # black
  DefColVal[DETACHED_ATTRIBUTE_COLOR = 8 ]  = [0, 0, 0] # black
  DefColVal[TEXT_COLOR               = 9 ]  = [0, 0, 0] # black
  DefColVal[BUS_COLOR                = 10]  = [0, 0, 0] # black
  DefColVal[SELECT_COLOR             = 11]  = [0, 0, 0] # black
  DefColVal[BOUNDINGBOX_COLOR        = 12]  = [0, 0, 0] # black
  DefColVal[ZOOM_BOX_COLOR           = 13]  = [0, 0, 0] # black
  DefColVal[STROKE_COLOR             = 14]  = [0, 0, 0] # black
  DefColVal[LOCK_COLOR               = 15]  = [0, 0, 0] # black
  DefColVal[OUTPUT_BACKGROUND_COLOR  = 16]  = [0, 0, 0] # black
  DefColVal[FREESTYLE1_COLOR         = 17]  = [0, 0, 0] # black
  DefColVal[FREESTYLE2_COLOR         = 18]  = [0, 0, 0] # black
  DefColVal[FREESTYLE3_COLOR         = 19]  = [0, 0, 0] # black
  DefColVal[FREESTYLE4_COLOR         = 20]  = [0, 0, 0] # black
  DefColVal[JUNCTION_COLOR           = 21]  = [0, 0, 0] # black
  DefColVal[PIN_HOT_END_COLOR        = 22]  = [1, 0, 0] # red
  DefColVal[MESH_GRID_MAJOR_COLOR    = 23]  = [0, 0, 0] # black
  DefColVal[MESH_GRID_MINOR_COLOR    = 24]  = [0, 0, 0] # black
  DefColVal[TEXT_MARK_COLOR          = 25]  = [0, 0, 0] # black

  DefPar = {
    :text_font_name => 'sans',
    :text_size_user_scale => 1,
    :text_size_system_scale => 15,
    :text_mark_size => 100,
    :text_mark_width => 1,
    :text_mark_visible_always => true,
    :text_field_transparent => true,
    :line_width_user_scale => 1,
    :line_width_system_scale => 15,
    :net_end_cap => GEDA_END_ROUND,
    :pin_hot_end_cap => GEDA_END_ROUND,
    :pin_cold_end_cap => GEDA_END_ROUND,
    :pin_hot_end_color => PIN_HOT_END_COLOR,
    :pin__color => PIN_COLOR, 
    :line_width_net => 1,
	  :text_size_scale => 1,
	  :line_width_scale => 1,
	  :show_pin_numbers => true
  }

class Dev
  SCREEN = 0
  PNG = 1
  PDF = 2
  SVG = 3
end

Dev_to_Index = {:SCREEN => 0, :PNG => 1, :PDF => 2, :SVG => 3}

class Schem
  def initialize()
    # gschems 24 RGB index colors
    @ColVal = [DefColVal, DefColVal, DefColVal, DefColVal]
	  
	  @Par = [DefPar, DefPar, DefPar, DefPar]
	  @Par[Dev::PNG] [:line_width] = 2 # pixel
	  #@Par[Dev::PNG] [:line_width_net] = 2 # pixel
	  
    @CR = nil
    @ObjectList = Array.new
    @SymDirs = DefaultSymDirs   
    @ThisLine = ''
    @Error=''
    @InputFile = nil
    @SymbolFile = nil
    @X_Range = Array.new
    @Y_Range = Array.new
    @bbox = BBOX.new
@log = Array.new
  end
  
  def InitColors(dev)
    if dev == 'PNG'
      @ColValPNG = DefColVal
    end  
  end

  def SetColor(dev, name, r, g, b)
    name = name.to_sym
    col = [r, g, b]
    if (col.max > 1.0) or (col.min < 0.0) then return false end
    if dev == 'PNG'
      if @ColValPNG.include?(name)
        @ColValPNG[name] = col
        return true
      else
        return false
      end
    else
      return false
    end
  end
  
  def ReadPreferences(filename)
  # read name, value
    if dev ==  'PNG' then end
  end



  # we should check for duplicates -- to be done
  def OpenSymFile(name)
    for base in @SymDirs
      n1, n2 = Dir.glob(File.join(base, '**', name))
      if n1
        @SymbolFile = File.open(n1, 'r')
        break
      end
    end
    return @SymbolFile
  end

  def NextLine()
    if @SymbolFile 
      if not (@ThisLine = @SymbolFile.gets)
        @SymbolFile.close
        @SymbolFile = nil
        @ThisLine = ']'
      end
    elsif not (@ThisLine = @InputFile.gets)
      @ThisLine = EOFC
    end
#    puts @ThisLine
  end

  def Pin?() return @ThisLine[0..0] == 'P' end

  def Net?() return @ThisLine[0..0] == 'N' end

  def FirstIs(c) return @ThisLine[0..0] == c end
  
  def ProcessVersion()
    if match = Regexp.new(VersionPat).match(@ThisLine)
      el = Versio.new
      el.version = match[2]
      el.fileformat_version = match[3]
      NextLine()
      return el
    else
      @Error = 'Invalid File Version'
    end
  end
  
  def ProcessLine()
    if match = Regexp.new(LinePat).match(@ThisLine)
      el = Line.new
      el.x1 = match[2].to_i
      el.y1 = match[3].to_i
      el.x2 = match[4].to_i
      el.y2 = match[5].to_i
      el.color = match[6].to_i
      el.width = match[7].to_i
      el.capstyle = match[8].to_i
      el.dashstyle = match[9].to_i
      el.dashlength = match[10].to_i
      el.dashspace = match[11].to_i
      @Error = el.check
      NextLine()
      return el
    else
      @Error = 'Invalid Line'
    end
  end

  def ProcessNetSegment()
    if match = Regexp.new(NetSegPat).match(@ThisLine)
      el = NetSegment.new
      el.x1 = match[2].to_i
      el.y1 = match[3].to_i
      el.x2 = match[4].to_i
      el.y2 = match[5].to_i
      el.color = match[6].to_i
      @Error = el.check
#@Error = ''
      NextLine()
      return el
    else
      @Error = 'Invalid Net'
    end
  end

  def ProcessBox()
    if match = Regexp.new(BoxPat).match(@ThisLine)
      el = Box.new
      el.x = match[2].to_i
      el.y = match[3].to_i
      el.width = match[4].to_i
      el.height = match[5].to_i
      el.color = match[6].to_i
      el.linewidth = match[7].to_i
      el.capstyle = match[8].to_i
      el.dashstyle = match[9].to_i
      el.dashlength = match[10].to_i
      el.dashspace = match[11].to_i
      el.filltype = match[12].to_i
      el.fillwidth = match[13].to_i
      el.angle1 = match[14].to_i
      el.pitch1 = match[15].to_i
      el.angle2 = match[16].to_i
      el.pitch2 = match[17].to_i
      NextLine()
      return el
    else
      @Error = 'Invalid Box'
    end
  end

  def ProcessCirc()
    if match = Regexp.new(CircPat).match(@ThisLine)
      el = Circ.new
      el.x = match[2].to_i
      el.y = match[3].to_i
      el.radius = match[4].to_i
      el.color = match[5].to_i
      el.width = match[6].to_i
      el.capstyle = match[7].to_i
      el.dashstyle = match[8].to_i
      el.dashlength = match[9].to_i
      el.dashspace = match[10].to_i
      el.filltype = match[11].to_i
      el.fillwidth = match[12].to_i
      el.angle1 = match[13].to_i
      el.pitch1 = match[14].to_i
      el.angle2 = match[15].to_i
      el.pitch2 = match[16].to_i
      NextLine()
      return el
    else
      @Error = 'Invalid Circle'
    end
  end

  def ProcessArc()
    NextLine()
  end
  
  def ProcessPath()
  end

  def ProcessText()
    if match = Regexp.new(TextPat).match(@ThisLine)
      el = Text.new
      el.x = match[2].to_i
      el.y = match[3].to_i
      el.color = match[4].to_i
      el.size = match[5].to_i
      el.visibility = match[6].to_i
      el.show_name_value = match[7].to_i
      el.angle = match[8].to_i
      el.alignment = match[9].to_i
      el.num_lines = match[10].to_i
      NextLine()
    else
      @Error = 'Text: Invalid start'
      return nil
    end
    el.lines = Array.new

    i = 0
    while i < el.num_lines
      el.lines.push @ThisLine
      NextLine()
      i += 1
    end

#    el.lines[0] = @ThisLine    
    
#    if match = Regexp.new(AttrPat).match(@ThisLine)
#      NextLine()
#    else
#      NextLine()
#    end

    return el
  end
  
  def ProcessAttributes
    if  FirstIs('{')
      NextLine()
      a = Array.new
      while true
        if FirstIs('}')
          NextLine()
          break
        else
          a.push(ProcessText())
        end
      end
      return a
    else
      @Error = 'Attr: Missing {'
      return nil
    end
  end
  
  def ProcessPin()
    if match = Regexp.new(PinPat).match(@ThisLine)
      el = Pin.new
      el.x1 = match[2].to_i
      el.y1 = match[3].to_i
      el.x2 = match[4].to_i
      el.y2 = match[5].to_i
      el.color = match[6].to_i
      el.pintype = match[7].to_i
      el.whichend = match[8].to_i
      NextLine()
    else
      @Error = 'Pin: Invalid start'
      return nil
    end
    if FirstIs('{')
      el.attributes = ProcessAttributes()
    end
    return el
  end

  def ProcessSym()
    if match = Regexp.new(EmbeddedSymPat).match(@ThisLine)
      el = EmbSym.new
      NextLine()
      if FirstIs('[')
        NextLine()
      else
        @Error = 'Symbol: Missing [ for embedded symbol'
        return nil
      end
    elsif match = Regexp.new(ExternSymPat).match(@ThisLine)
      el = Sym.new
      if OpenSymFile(match[7])
        @SymFileName = match[7]
        NextLine()
      else
        @Error = 'Symbol: File not found'
        return nil
      end
    else
      @Error = 'Symbol: Invalid start'
      return nil
    end

    el.x = match[2].to_i
    el.y = match[3].to_i
    el.selectable = match[4].to_i
    el.angle = match[5].to_i
    el.mirror = match[6].to_i
    el.basename = match[7]   
    while true
      if FirstIs(']')
        NextLine()
        break
      elsif FirstIs(VersionChar)
        el.components.push(ProcessVersion())
      elsif FirstIs(LineChar)
        el.components.push(ProcessLine())
      elsif FirstIs(NetSegChar)
        el.components.push(ProcessNetSeg())
      elsif FirstIs(BoxChar)
        el.components.push(ProcessBox())
      elsif FirstIs(CircChar)
        el.components.push(ProcessCirc())
      elsif FirstIs(ArcChar)
        el.components.push(ProcessArc())
      elsif FirstIs(PathChar)
        el.components.push(ProcessPath())
      elsif FirstIs(TextChar)
        el.components.push(ProcessText())
      elsif FirstIs(PinChar)
        el.components.push(ProcessPin())
      else
        @Error = 'Symbol: Syntax error'
        return nil
      end
    end
    if FirstIs('{')
      el.attributes = ProcessAttributes()
    end
if el.class == Sym
el.components.each{|o| o.set_box; o.translate(el.x, el.y)}
end
    return el
  end

  def ProcessInputFile(name)
    begin
      @SymFileName = ''
      @Error = ''
      @InputFile = File.open(name, 'r')
      NextLine()
      while (@Error == '') and not FirstIs(EOFC)
        if FirstIs(VersionChar)
          @ObjectList.push(ProcessVersion())
        elsif FirstIs(LineChar)
          @ObjectList.push(ProcessLine())
        elsif FirstIs(NetSegChar)
          @ObjectList.push(ProcessNetSegment())
        elsif FirstIs(BoxChar)
          @ObjectList.push(ProcessBox())
        elsif FirstIs(CircChar)
          @ObjectList.push(ProcessCirc())
        elsif FirstIs(ArcChar)
          @ObjectList.push(ProcessArc())
        elsif FirstIs(PathChar)
          @ObjectList .push(ProcessPath())
        elsif FirstIs(TextChar)
          @ObjectList.push(ProcessText())
        elsif FirstIs(SymChar)
          @ObjectList.push(ProcessSym())
        else
          @Error = 'Syntax error'
          break
        end
      end
# if @Error[0] == ' ' then only a warning...
# we should log it...
      if @Error != ''
        if @SymbolFile
          print 'Error in file ', @SymFileName, ': ', @Error, "\n"
          print '-=> Line ', @SymbolFile.lineno, ': ', @ThisLine, "\n"
          @SymbolFile.close
        else
          print 'Error in file ', name, ': ', @Error, "\n"
          print '-=> Line ', @InputFile.lineno, ': ', @ThisLine, "\n"
        end 
      end
      @InputFile.close
#@ObjectList.each{|o| o.add_x_range(@X_Range); o.add_y_range(@Y_Range)}

@ObjectList.each{|o| o.set_box; o.enlarge_bbox(@bbox)}
#puts @bbox
#Process.exit
    rescue => e
      puts e.message
    end
  end

  def write_png(name, x, y)
    scale = [x.to_f / (@bbox.x2 - @bbox.x1), y.to_f / (@bbox.y2 - @bbox.y1)].min
    @Par[Dev::PNG][:lws] = 1.0 / scale
    surface = Cairo::ImageSurface.new(Cairo::FORMAT_ARGB32, x, y)
    @CR = Cairo::Context.new(surface)
#@CR.line_width_dev_min = 0.5
    #@CR.sharp_lines = false
    #@CR.line_width_add_on = 1
#@CR.set_antialias(Cairo::ANTIALIAS_NONE)
#@CR.line_width_to_display = Proc.new{|l,w| 1}
#@CR.display_size = [x, y].min
#@CR.line_width_to_display =  @CR.method(:line_width_to_display_scaled)
    #@CR.set_source_rgb(*@colors[:pin])
    @CR.translate(0, y)    
    @CR.scale(1, -1)

    @CR.scale(scale, scale)




#aaa, bbb = @CR.user_to_device(1, 1)
#print scale, ' ', aaa, ' ',bbb, "\n"

#Process.exit



    
    @CR.set_source_rgb(0.8,0.8,0.8)
    @CR.paint
    @CR.set_source_rgb(0,0,1)
    @CR.set_line_width(20)
    @CR.move_to(0,0)
    @CR.line_to(20,20)
    @CR.stroke    
    @CR.translate(-@bbox.x1, -@bbox.y1)
  @CR.set_line_width(200)

  @ObjectList.each{|x| x.draw(@CR, @ColVal[Dev::PNG], @Par[Dev::PNG]) unless x == nil}

    surface.write_to_png(name)
    @CR.destroy
    surface.destroy
  end

  def write(name)
    begin
      file = File.open(name, 'w')
      @ObjectList.each{|x| x.write(file)}
      file.close
    rescue => e
      puts e.message
    end
  end  

end # Schem

def main
  if (ARGV[0] == nil) or (ARGV[0] == '-h') or (ARGV[0] == '--help')
    print Docu
  else
    s1 = Schem.new
    s1.ProcessInputFile(ARGV[0])
    #s1.testpng
s1.write_png('out.png', 800,800)
    s1.write('txt.txt')
  end
end

# Start processing after all functions are read
main

