simplepath.rb revision dcd18983a1972be721248b6d5b771cf338739415
#!/usr/bin/env ruby
# simplepath.rb
# functions for digesting paths into a simple list structure
#
# Ruby port by MenTaLguY
#
# Copyright (C) 2005 Aaron Spike <aaron@ekips.org>
# Copyright (C) 2006 MenTaLguY <mental@rydia.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
require 'strscan'
def lexPath(d)
# iterator which breaks path data
# identifies command and parameter tokens
scanner = StringScanner.new(d)
delim = /[ \t\r\n,]+/
command = /[MLHVCSQTAZmlhvcsqtaz]/
parameter = /(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/
until scanner.eos?
scanner.skip(delim)
if m = scanner.scan(command)
yield m, true
elsif m = scanner.scan(parameter)
yield m, false
else
#TODO: create new exception
raise 'Invalid path data!'
end
end
end
PathDef = Struct.new :implicit_next, :param_count, :casts, :coord_types
PATHDEFS = {
'M' => PathDef['L', 2, [:to_f, :to_f], [:x,:y]],
'L' => PathDef['L', 2, [:to_f, :to_f], [:x,:y]],
'H' => PathDef['H', 1, [:to_f], [:x]],
'V' => PathDef['V', 1, [:to_f], [:y]],
'C' => PathDef['C', 6, [:to_f, :to_f, :to_f, :to_f, :to_f, :to_f], [:x,:y,:x,:y,:x,:y]],
'S' => PathDef['S', 4, [:to_f, :to_f, :to_f, :to_f], [:x,:y,:x,:y]],
'Q' => PathDef['Q', 4, [:to_f, :to_f, :to_f, :to_f], [:x,:y,:x,:y]],
'T' => PathDef['T', 2, [:to_f, :to_f], [:x,:y]],
'A' => PathDef['A', 7, [:to_f, :to_f, :to_f, :to_i, :to_i, :to_f, :to_f], [0,0,0,0,0,:x,:y]],
'Z' => PathDef['L', 0, [], []]
}
def parsePath(d)
# Parse SVG path and return an array of segments.
# Removes all shorthand notation.
# Converts coordinates to absolute.
retval = []
command = nil
outputCommand = nil
params = []
pen = [0.0,0.0]
subPathStart = pen
lastControl = pen
lastCommand = nil
lexPath(d) do |token, isCommand|
unless command
if isCommand
if lastCommand or token.upcase == 'M'
command = token
else
raise 'Invalid path, must begin with moveto.'
end
else
#command was omited
#use last command's implicit next command
if lastCommand
if lastCommand =~ /[A-Z]/
command = PATHDEFS[lastCommand].implicit_next
else
command = PATHDEFS[lastCommand.upcase].implicit_next.downcase
end
else
raise 'Invalid path, no initial command.'
end
end
outputCommand = command.upcase
else
raise 'Invalid number of parameters' if isCommand
param = token.send PATHDEFS[outputCommand].casts[params.length]
if command =~ /[a-z]/
case PATHDEFS[outputCommand].coord_types[params.length]
when :x: param += pen[0]
when :y: param += pen[1]
end
end
params.push param
end
if params.length == PATHDEFS[outputCommand].param_count
#Flesh out shortcut notation
case outputCommand
when 'H','V'
case outputCommand
when 'H': params.push pen[1]
when 'V': params.unshift pen[0]
end
outputCommand = 'L'
when 'S','T'
params.unshift(pen[1]+(pen[1]-lastControl[1]))
params.unshift(pen[0]+(pen[0]-lastControl[0]))
case outputCommand
when 'S': outputCommand = 'C'
when 'T': outputCommand = 'Q'
end
end
#current values become "last" values
case outputCommand
when 'M'
subPathStart = params[0,2]
pen = subPathStart
when 'Z'
pen = subPathStart
else
pen = params[-2,2]
end
case outputCommand
when 'Q','C'
lastControl = params[-4,2]
else
lastControl = pen
end
lastCommand = command
retval.push [outputCommand,params]
command = nil
params = []
end
end
raise 'Unexpected end of path' if command
return retval
end
def formatPath(a)
# Format SVG path data from an array
a.map { |cmd,params| "#{cmd} #{params.join(' ')}" }.join
end
def translatePath(p, x, y)
p.each do |cmd,params|
coord_types = PATHDEFS[cmd].coord_types
for i in 0...(params.length)
case coord_types[i]
when :x: params[i] += x
when :y: params[i] += y
end
end
end
end
def scalePath(p, x, y)
p.each do |cmd,params|
coord_types = PATHDEFS[cmd].coord_types
for i in 0...(params.length)
case coord_types[i]
when :x: params[i] *= x
when :y: params[i] *= y
end
end
end
end
def rotatePath(p, a, cx = 0, cy = 0)
return p if a == 0
p.each do |cmd,params|
coord_types = PATHDEFS[cmd].coord_types
for i in 0...(params.length)
if coord_types[i] == :x
x = params[i] - cx
y = params[i + 1] - cy
r = Math.sqrt((x**2) + (y**2))
unless r.zero?
theta = Math.atan2(y, x) + a
params[i] = (r * Math.cos(theta)) + cx
params[i + 1] = (r * Math.sin(theta)) + cy
end
end
end
end
end